package com.geek.gkfgw.web.aspect;

import com.geek.gkfgw.biz.redis.RecvTokenInfoRedisListener;
import com.geek.gkfgw.framework.cache.redis.RedisSender;
import com.geek.gkfgw.framework.cache.util.EhcacheUtils;
import com.geek.gkfgw.framework.cache.util.RedisUtils;
import com.geek.gkfgw.service.token.bean.RedisTokenMsg;
import com.geek.gkfgw.service.token.bean.TokenInfo;
import com.geek.gkfgw.util.enums.redis.RedisTopicEnum;
import com.geek.gkfgw.util.excp.GkFgwCodeEnum;
import com.geek.gkfgw.util.excp.GkFgwException;
import com.geek.gkfgw.util.json.JacksonUtils;
import com.geek.gkfgw.web.anno.TokenIgnore;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * <h1>Token 拦截器，专门用于令牌校验所需。</h1><br/>
 *
 * <h1><br/><a href="https://u.geekbang.org/subject/arch3/100614201">Java 业务架构实战营：https://u.geekbang.org/subject/arch3/100614201</a></h1><br/><h1><a href="https://gitee.com/ylimhhmily/BizArchPracticeCamp">Java 业务架构实战营代码地址：https://gitee.com/ylimhhmily/BizArchPracticeCamp</a></h1><br/><h1><a href="https://u.geekbang.org/subject/java4th/1001148?source=app_share">高级 Java 工程体系课 2023 版：https://u.geekbang.org/subject/java4th/1001148?source=app_share</a></h1><br/><h1><a href="https://time.geekbang.org/column/intro/100312101?tab=catalog">Dubbo 源码剖析与实战：https://time.geekbang.org/column/intro/100312101?tab=catalog</a></h1>
 *
 * @author hmilyylimh
 * ^_^
 * @version 0.0.1
 * ^_^
 * @date 2023/9/3 22:30
 */
@Component
public class TokenInterceptor implements HandlerInterceptor {

    /** <h2>前端上送的请求头 key，含义就是短令牌的 key</h2> **/
    private static final String ACCESS_TOKEN = "accesstoken";

    /** <h2>令牌存储在 ehcache、redis 中 key 的前缀</h2> **/
    private static final String CACHE_TOKEN_PREFIX = RecvTokenInfoRedisListener.CACHE_TOKEN_PREFIX;

    /** <h2>仿造一个空数据，即一个失效数据，解决频繁撞库的风险</h2> **/
    private static final TokenInfo EMPTY = TokenInfo.generateInvalidTokenInfo();

    // 优化过后的代码
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 0. 有 TokenIgnore 注解的话，放行，直接走后续业务逻辑
        if (((HandlerMethod) handler).getMethodAnnotation(TokenIgnore.class) != null) {
            return true;
        }

        // 1. 获取 accessToken
        String accssToken = extractAccessToken(request);

        // 2. 检查 Ehcache 缓存结果，有就通过，没有继续往下走
        TokenInfo tokenInfo = checkEhcacheThrowIfExpired(accssToken);
        if (tokenInfo.isAlived()) {
            return true;
        }

        // 3. 检查 Redis 缓存结果，没有制造一份失效数据，有就继续往下走，总之得到了一份数据
        tokenInfo = RedisUtils.get(CACHE_TOKEN_PREFIX + accssToken, TokenInfo.class);
        if (tokenInfo != null) {
            // 4. 通过 Redis 消息订阅频道，发送消息
            publishRedisMessage(accssToken, tokenInfo);
            // 5. 最后返回 true 表示验证一切正常，没有任何问题
            return true;
        }

        // 生成失效数据，并通知其他节点更新
        tokenInfo = generateInvalidTokenInfo();
        publishRedisMessage(accssToken, tokenInfo);
        throw new GkFgwException(GkFgwCodeEnum.INVALID_TOKEN);
    }

    private void publishRedisMessage(String accessToken, TokenInfo tokenInfo) {
        String redisMsg = JacksonUtils.toJson(RedisTokenMsg.create(accessToken, tokenInfo));
        RedisSender.sendTopic(RedisTopicEnum.ACCESS_TOKEN.getCode(), redisMsg);
    }

    private TokenInfo generateInvalidTokenInfo() {
        return TokenInfo.generateInvalidTokenInfo();
    }

    // 初次写的代码
    public boolean preHandle2(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取 accessToken
        String accessToken = extractAccessToken(request);

        // 2. 检查 Ehcache 缓存结果，有就通过，没有继续往下走
        TokenInfo tokenInfo = EhcacheUtils.get(CACHE_TOKEN_PREFIX + accessToken, TokenInfo.class);
        boolean isExpired = tokenInfo.isExpired();
        if (tokenInfo == null) {
            // donoting
        } else if (tokenInfo.isExpired()) {
            throw new GkFgwException(GkFgwCodeEnum.INVALID_TOKEN);
        } else {
            return true;
        }

        // 3. 检查 Redis 缓存结果，没有制造一份失效数据，有就继续往下走，总之得到了一份数据
        tokenInfo = RedisUtils.get(CACHE_TOKEN_PREFIX + accessToken, TokenInfo.class);
        if (tokenInfo == null) {
            tokenInfo = generateInvalidTokenInfo();
            publishRedisMessage(accessToken, tokenInfo);
            throw new GkFgwException(GkFgwCodeEnum.INVALID_TOKEN);
        }

        // 4. 通过 Redis 消息订阅频道，发送消息
        publishRedisMessage(accessToken, tokenInfo);

        // 5. 最后返回 true 表示验证一切正常，没有任何问题
        return true;
    }

    private TokenInfo checkEhcacheThrowIfExpired(String accssToken) {
        TokenInfo tokenInfo = EhcacheUtils.get(CACHE_TOKEN_PREFIX + accssToken, TokenInfo.class);
        if (tokenInfo == null) {
            return EMPTY;
        }

        if (tokenInfo.isExpired()) {
            throw new GkFgwException(GkFgwCodeEnum.INVALID_TOKEN);
        }

        return tokenInfo;
    }

    private String extractAccessToken(HttpServletRequest request) {
        return request.getHeader(ACCESS_TOKEN);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}
